// 版权归Go作者所有。版权所有。
// 此源代码的使用受BSD样式的约束
// 可以在许可证文件中找到的许可证。

// TODO（gri）：此文件和文件src/cmd/gofmt/internal.go是
// 相同（但此注释和包名称除外）。不要修改
// 一个没有另一个。确定我们是否可以考虑功能
// 在公共API中。有关上下文，请参见#11844。

package format

import (
	"bytes"
	"go/ast"
	"go/parser"
	"go/printer"
	"go/token"
	"strings"
)

// parse解析从命名文件读取的src，
// 作为Go源文件、声明或语句列表。
func parse(fset *token.FileSet, filename string, src []byte, fragmentOk bool) (
	file *ast.File,
	sourceAdj func(src []byte, indent int) []byte,
	indentAdj int,
	err error,
) {
	// 尝试作为整个源文件。
	file, err = parser.ParseFile(fset, filename, src, parserMode)
	// 如果没有错误，请返回。如果错误是源文件不是以
	// 包行和源代码片段都正常，通过
	// 尝试作为源片段。停止并返回任何其他错误。
	if err == nil || !fragmentOk || !strings.Contains(err.Error(), "expected 'package'") {
		return
	}

	// 如果这是一个声明列表，请将其设置为源文件
	// 通过插入一个package子句。
	// 使用“；”插入，不是换行符，所以行号
	// 在psrc中，与src中的匹配。
	psrc := append([]byte("package p;"), src...)
	file, err = parser.ParseFile(fset, filename, psrc, parserMode)
	if err == nil {
		sourceAdj = func(src []byte, indent int) []byte {
			// 删除package子句。
			// 戈夫特把‘；’变成一个“\n”。
			src = src[indent+len("package p\n"):]
			return bytes.TrimSpace(src)
		}
		return
	}
	// 如果错误是源文件不是以
	// 声明，作为语句列表进行尝试。
	// 停止并返回任何其他错误。
	if !strings.Contains(err.Error(), "expected declaration") {
		return
	}

	// 如果这是一个语句列表，请将其设置为源文件
	// 通过插入一个package子句并旋转列表
	// 变成一个函数体。这也处理表达式。
	// 使用“；”插入，不是换行符，所以行号
	// 在fsrc中，与src中的匹配。在“}”之前添加一个额外的“\n”
	// 确保在“}”之前刷新注释。
	fsrc := append(append([]byte("package p; func _() {"), src...), '\n', '\n', '}')
	file, err = parser.ParseFile(fset, filename, fsrc, parserMode)
	if err == nil {
		sourceAdj = func(src []byte, indent int) []byte {
			// 上限调整缩进为零。
			if indent < 0 {
				indent = 0
			}
			// 拆下包装。
			// Gofmt已将“；”改为“\n\n”。
			// 将有两个带缩进的非空行，因此缩进为2*。
			src = src[2*indent+len("package p\n\nfunc _() {"):]
			// 仅删除“}\n”后缀：剩余的空格仍将被修剪
			src = src[:len(src)-len("}\n")]
			return bytes.TrimSpace(src)
		}
		// Gofmt还将函数体缩进了一级。
		// 用缩进调整。
		indentAdj = -1
	}

	// 成功，或没有选择。
	return
}

// 格式化最初从src获得的给定包文件
// 并通过sourceAdj基于原始源调整结果
// 和缩进。
func format(
	fset *token.FileSet,
	file *ast.File,
	sourceAdj func(src []byte, indent int) []byte,
	indentAdj int,
	src []byte,
	cfg printer.Config,
) ([]byte, error) {
	if sourceAdj == nil {
		// 完整的源文件。
		var buf bytes.Buffer
		err := cfg.Fprint(&buf, fset, file)
		if err != nil {
			return nil, err
		}
		return buf.Bytes(), nil
	}

	// 部分源文件。
	// 确定并预先设定前导间距。
	i, j := 0, 0
	for j < len(src) && isSpace(src[j]) {
		if src[j] == '\n' {
			i = j + 1 // 前导空间中最后一行的字节偏移量
		}
		j++
	}
	var res []byte
	res = append(res, src[:i]...)

	// 确定第一个代码行的缩进并在缩进前加上前缀。
	// 除非没有选项卡，否则将忽略空格，
	// 在这种情况下，空格作为一个选项卡计算。
	indent := 0
	hasSpace := false
	for _, b := range src[i:j] {
		switch b {
		case ' ':
			hasSpace = true
		case '\t':
			indent++
		}
	}
	if indent == 0 && hasSpace {
		indent = 1
	}
	for i := 0; i < indent; i++ {
		res = append(res, '\t')
	}

	// 格式化源。
	// 写它时不带任何前导和尾随空格。
	cfg.Indent = indent + indentAdj
	var buf bytes.Buffer
	err := cfg.Fprint(&buf, fset, file)
	if err != nil {
		return nil, err
	}
	out := sourceAdj(buf.Bytes(), cfg.Indent)

	// 如果调整后的输出为空，则源
	// 是空的，但（可能）是空白。
	// 结果是传入源。
	if len(out) == 0 {
		return src, nil
	}

	// 否则，将输出追加到前导空格。
	res = append(res, out...)

	// 确定并追加尾随空格。
	i = len(src)
	for i > 0 && isSpace(src[i-1]) {
		i--
	}
	return append(res, src[i:]...), nil
}

// isSpace报告字节是否为空格字符。
// isSpace将空间定义为以下字节之一：''\t'、'\n'和'\r'。
func isSpace(b byte) bool {
	return b == ' ' || b == '\t' || b == '\n' || b == '\r'
}
